円グラフ#

円グラフPie Chart) とは、主に質的変数に対して、全体に対する内訳を扇形の 角度 [1]で表現する手法です。 扇形グラフ とも呼ばれます。

pie

上図の、アニメデータ中の声優の性別の割合を表した円グラフを例に説明します。 円グラフでは、 スケールで質的変数(上図「声優の性別」)の水準(上図「female」)を指定し、それに対応する 位置(角度) [2]スケール(上図「位置(角度)」)で全体に占める割合を表現します。

円グラフは、内訳を可視化する際に最もよく用いられる手法の一つです。 それぞれの水準が全体に占める割合を直感的に表現できるという長所があります。 一方で、三つ以上の水準を持つ質的変数に対して適用すると、大小関係を比較しづらくなるという短所があります。 これは、角度の基準が固定されない水準が現れるためです。 ただし、例外として、水準間に大きな差がある場合は円グラフが有効に働くこともあります。

質的変数の水準が少ない場合や、全体に対する内訳であるということを強調したい場合を除き、円グラフの採用には慎重になったほうが良い[3]でしょう。

Plotlyでは、plotly.express.pie()で円グラフを作成できます。

# plotly.expressモジュールをpxという名前でインポート
# 簡単にインタラクティブな図を作成するためのモジュール
import plotly.express as px

# px.histogram関数を使用して、ヒストグラムを作成
# dfデータフレームのcol_valカラムの数量の比率を、
# col_nameカラムの名称を用いて円グラフを表示する
# 作成した図はfigという変数に保存される
fig = px.pie(df, values="col_val", names="col_name")

初期設定#

以降では、マンガ・アニメ・ゲームデータを可視化するための初期設定を行います。 なお、紙幅の都合のため、書籍版と一部構成が異なることにご注意ください。

Import#

必要なライブラリをImportします。

Hide code cell content
# warningsモジュールのインポート
import warnings

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px

# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure

なお、型ヒントについてはこちらを参照ください。

定数#

本Notebookで用いる定数を定義します。 なお、Pythonにおける定数の扱いについては、こちらを参照ください。

Hide code cell content
# マンガデータ保存ディレクトリのパス
DIR_CM = Path("../../data/cm/input")
# アニメデータ保存ディレクトリのパス
DIR_AN = Path("../../data/an/input")
# ゲームデータ保存ディレクトリのパス
DIR_GM = Path("../../data/gm/input")

# マンガデータの分析結果の出力先ディレクトリのパス
DIR_OUT_CM = DIR_CM.parent / "output" / Path.cwd().parts[-1] / "pie"
# アニメデータの分析結果の出力先ディレクトリのパス
DIR_OUT_AN = DIR_AN.parent / "output" / Path.cwd().parts[-1] / "pie"
# ゲームデータの分析結果の出力先ディレクトリのパス
DIR_OUT_GM = DIR_GM.parent / "output" / Path.cwd().parts[-1] / "pie"
Hide code cell content
# 読み込み対象ファイル名の定義

# マンガ作品とマンガ作者の対応関係に関するファイル
FN_CC_CRT = "cm_cc_crt.csv"

# マンガ各話に関するファイル
FN_CE = "cm_ce.csv"

# アニメ作品と原作者の対応関係に関するファイル
FN_AC_ACT = "an_ac_act.csv"

# アニメ各話に関するファイル
FN_AE = "an_ae.csv"

# ゲームパッケージとプラットフォームの対応関係に関するファイル
FN_PKG_PF = "gm_pkg_pf.csv"
Hide code cell content
# plotlyの描画設定の定義

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
Hide code cell content
# 可視化に関する設定値の定義

# 「年代」の集計単位
UNIT_YEARS = 10
Hide code cell content
# pandasのweekday関数で取得できる曜日の数値と実際の曜日名を対応させる辞書を定義
# 0:月曜日, 1:火曜日, ... , 6:日曜日
WEEKDAY2YOBI = {
    0: "月",
    1: "火",
    2: "水",
    3: "木",
    4: "金",
    5: "土",
    6: "日",
}
Hide code cell content
# 質的変数の描画用のカラースケールの定義

# Okabe and Ito (2008)基準のカラーパレット
# 色の識別性が高く、多様な色覚の人々にも見やすい色組み合わせ
# 参考URL: https://jfly.uni-koeln.de/color/#pallet
OKABE_ITO = [
    "#000000",  # 黒 (Black)
    "#E69F00",  # 橙 (Orange)
    "#56B4E9",  # 薄青 (Sky Blue)
    "#009E73",  # 青緑 (Bluish Green)
    "#F0E442",  # 黄色 (Yellow)
    "#0072B2",  # 青 (Blue)
    "#D55E00",  # 赤紫 (Vermilion)
    "#CC79A7",  # 紫 (Reddish Purple)
]

関数#

以下では、本Notebookで用いる関数を定義します。

Hide code cell content
def show_fig(fig: Figure) -> None:
    """
    所定のレンダラーを用いてplotlyの図を表示
    Jupyter Bookなどの環境での正確な表示を目的とする

    Parameters
    ----------
    fig : Figure
        表示対象のplotly図

    Returns
    -------
    None
    """

    # 図の周囲の余白を設定
    # t: 上余白
    # l: 左余白
    # r: 右余白
    # b: 下余白
    fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))

    # 所定のレンダラーで図を表示
    fig.show(renderer=RENDERER)
Hide code cell content
def add_years_to_df(
    df: pd.DataFrame, unit_years: int = UNIT_YEARS, col_date: str = "date"
) -> pd.DataFrame:
    """
    データフレームにunit_years単位で区切った年数を示す新しい列を追加

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    unit_years : int, optional
        年数を区切る単位、デフォルトはUNIT_YEARS
    col_date : str, optional
        日付を含むカラム名、デフォルトは "date"

    Returns
    -------
    pd.DataFrame
        新しい列が追加されたデータフレーム
    """

    # 入力データフレームをコピー
    df_new = df.copy()

    # unit_years単位で年数を区切り、新しい列として追加
    df_new["years"] = (
        pd.to_datetime(df_new[col_date]).dt.year // unit_years * unit_years
    )

    # 'years'列のデータ型を文字列に変更
    df_new["years"] = df_new["years"].astype(str)

    return df_new
Hide code cell content
def save_df_to_csv(df: pd.DataFrame, dir_save: Path, fn_save: str) -> None:
    """
    DataFrameをCSVファイルとして指定されたディレクトリに保存する関数

    Parameters
    ----------
    df : pd.DataFrame
        保存対象となるDataFrame
    dir_save : Path
        出力先ディレクトリのパス
    fn_save : str
        保存するCSVファイルの名前(拡張子は含めない)
    """
    # 出力先ディレクトリが存在しない場合は作成
    dir_save.mkdir(parents=True, exist_ok=True)

    # 出力先のパスを作成
    p_save = dir_save / f"{fn_save}.csv"

    # DataFrameをCSVファイルとして保存する
    df.to_csv(p_save, index=False, encoding="utf-8-sig")

    # 保存完了のメッセージを表示する
    print(f"DataFrame is saved as '{p_save}'.")

可視化例#

マンガデータ#

雑誌ごとのマンガ作者数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_cc_crt = pd.read_csv(DIR_CM / FN_CC_CRT)

重複してカウントされないよう、複数のマンガ雑誌に掲載経験のあるマンガ作者を可視化対象から除外します。

Hide code cell content
# crtidごとのmcid数を集計し、n_mc列として追加した中間DataFrame
df_tmp = df_cc_crt.groupby("crtid")["mcid"].nunique().reset_index(name="n_mc")
# 複数のマンガ雑誌に掲載経験のあるcrtid一覧をリスト化
crtids2drop = df_tmp[df_tmp["n_mc"] > 1]["crtid"].to_list()
# df_cc_crtからcrtid2dropに含まれる行を削除
df_cc_crt = df_cc_crt[~df_cc_crt["crtid"].isin(crtids2drop)].reset_index(drop=True)

念のため、assert文で確認します。

Hide code cell content
# 各crtidに対して、関連するmcnameのユニークな数を数える
# groupbyでcrtidごとにグループ化し、nuniqueメソッドで各グループのmcnameのユニークな数を数える
# all関数を使って、nuniqueの結果が全て1であること(各crtidが1つのmcnameにのみ紐づいていること)を確認
assert all(df_cc_crt.groupby("crtid")["mcname"].nunique() == 1)

AssertionErrorが出なかったため、このまま分析を進めます。

Hide code cell content
# マンガ雑誌ごとのユニークな原作者数を集計

# マンガ雑誌名(mcname)ごとに、ユニークな原作者名(crtname)の数を集計
# reset_index(name="n_crt")を使用して集計結果を新しいデータフレームに変換し、
# 集計された原作者数を "n_crt" という列名で保存
df_cm = df_cc_crt.groupby("mcname")["crtname"].nunique().reset_index(name="n_crt")

# 列名をわかりやすく変更
df_cm = df_cm.rename(columns={"mcname": "マンガ雑誌名", "n_crt": "作者数"})

# 円グラフの表示順序が、その割合が大きい順序になるように制御
orders_cm = {
    "マンガ雑誌名": df_cm.sort_values("作者数", ascending=False)[
        "マンガ雑誌名"
    ].tolist()
}
Hide code cell content
# 可視化対象のDataFrameを確認
df_cm.head()
マンガ雑誌名 作者数
0 週刊少年サンデー 593
1 週刊少年ジャンプ 849
2 週刊少年チャンピオン 755
3 週刊少年マガジン 670
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm, DIR_OUT_CM, "cm")
DataFrame is saved as '../../data/cm/output/07/pie/cm.csv'.
Hide code cell source
# マンガ雑誌ごとの原作者数を円グラフで可視化

# px.pie関数を使用して、各マンガ雑誌ごとの原作者数の円グラフを作成
# "作者数" を値として、"マンガ雑誌名" を名前として表示
# category_ordersにより各水準の表示順序を制御
# color_discrete_sequenceにはOKABE_ITOカラースケールを使用
fig = px.pie(
    df_cm,
    values="作者数",
    names="マンガ雑誌名",
    category_orders=orders_cm,
    color_discrete_sequence=OKABE_ITO,
)

# 凡例名を追加
fig.update_layout(legend_title_text="マンガ雑誌名")

# 作成した円グラフを表示
show_fig(fig)

上図は、マンガ雑誌ごとの合計マンガ作者数の内訳を表現した円グラフです。

第一印象として、週刊少年ジャンプは比較的多いものの、マンガ雑誌間に圧倒的な違いはないように見えます。 ここまで拮抗していると、アノテーション(図中のパーセント表示)を見なければ各雑誌の大小関係を比較することは困難です。 例えば、週刊少年サンデー20.7%週刊少年マガジン23.4%をそれぞれ占めていますが、 文字を見ずにその大小関係を把握できるでしょうか?

このように、水準が三つ以上ある質的変数に対する円グラフの採用は、一度立ち止まって考えたほうが良いでしょう。 場合によっては、後述する棒グラフ等他の手法を検討しましょう。

ただし、例外的に、ゲームデータで例示するような水準間の差が大きい場合は、水準が三つ以上あっても有効です。

次に、年代別に作者数のシェアの推移を表現してみましょう。 まず、年代情報の追加に必要なファイルを読み込みます。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ce = pd.read_csv(DIR_CM / FN_CE)
Hide code cell content
# df_ceに年代情報を追加するための関数add_years_to_dfを適用
# これにより、df_ceに年代情報が含まれるようになる
df_ce = add_years_to_df(df_ce)

# 年代(years)とccidのユニークな組み合わせを持つデータフレームを作成
# 重複するデータを削除し、必要なカラムのみを選択
# ignore_index=Trueで新しいインデックスを割り当てている
df_cc_years = df_ce.drop_duplicates(subset=["years", "ccid"], ignore_index=True)[
    ["ccid", "years"]
]
Hide code cell content
# df_cc_crtとdf_cc_yearsをccidを基準にして右結合(right join)し、df_mergeを作成
# これにより、df_cc_crtのデータにdf_cc_yearsの年代情報が組み合わされる
df_merge = pd.merge(df_cc_crt, df_cc_years, on="ccid", how="right")

# df_mergeをマンガ雑誌名と年代ごとにグループ化し、作者のユニーク数を集計
# nunique()を使用して各グループ内のユニークな作者数をカウント
df_cm2 = (
    df_merge.groupby(["mcname", "years"])["crtid"].nunique().reset_index(name="n_crt")
)

# 列名をわかりやすい名前に変更
# mcnameをマンガ雑誌名、yearsを年代、n_crtを作者数にリネーム
df_cm2 = df_cm2.rename(
    columns={"mcname": "マンガ雑誌名", "years": "年代", "n_crt": "作者数"}
)

内訳を計算するとき、年代別のマンガ作者数の合計値を分母として用います。 分母が小さいと内訳が極端な値を取りやすくなるため、可視化には向きません。

事前に年代ごとのマンガ作者数を集計しておきましょう。

Hide code cell content
# 年代別の合計作者数を念のため確認
df_cm2.groupby("年代")["作者数"].sum().reset_index()
年代 作者数
0 1970 493
1 1980 682
2 1990 650
3 2000 787
4 2010 881

極端にマンガ作者数が少ない年代はありませんでした。 このデータの全てを可視化対象として問題なさそうです。

Hide code cell content
# 可視化対象のDataFrameを確認
df_cm2.head()
マンガ雑誌名 年代 作者数
0 週刊少年サンデー 1970 95
1 週刊少年サンデー 1980 157
2 週刊少年サンデー 1990 147
3 週刊少年サンデー 2000 162
4 週刊少年サンデー 2010 179
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm2, DIR_OUT_CM, "cm2")
DataFrame is saved as '../../data/cm/output/07/pie/cm2.csv'.
Hide code cell source
# マンガ雑誌ごとの原作者数を円グラフで可視化
# valuesに作者数、namesにマンガ雑誌名を指定し、年代ごとにファセット分割
# category_ordersでカテゴリの順序を指定し、color_discrete_sequenceで色を設定
fig = px.pie(
    df_cm2,
    values="作者数",
    names="マンガ雑誌名",
    facet_col="年代",
    category_orders=orders_cm,
    color_discrete_sequence=OKABE_ITO,
)

# 円グラフの凡例のタイトルを「マンガ雑誌名」に設定
fig.update_layout(legend_title_text="マンガ雑誌名")

# ファセット(年代ごとの円グラフ)のタイトルを簡潔にする処理
# デフォルトではタイトルは「年代=xxx」という形式になっている
# この処理は「=」で文字列を分割して「xxx」の部分だけを取り出し、
# タイトルを「xxx」だけの簡潔な形に更新している
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成した円グラフを表示
show_fig(fig)

上の図は、マンガ雑誌ごとのマンガ作者数の内訳を、年代別を表現した円グラフです。 第一印象として、1970年代の週刊少年ジャンプが比較的多くのマンガ作者を抱えていたことがわかります。

全て のマンガ雑誌の年代別の推移をすぐに把握することは困難です。 強く意識すれば、角度の基準(始点あるいは終点)が12時の方向に固定されている週刊少年ジャンプ週刊少年サンデーに関しては年代別の推移を追えるかもしれません。 しかし、週刊少年チャンピオン週刊少年マガジンは基準が変動するため、その推移を追うことは不可能です。 もちろん図中のアノテーションを読めば定量的な比較は可能ですが、円グラフとして可視化することの意義は薄れます。 繰り返しになりますが、質的変数の水準数が二つしかない場合か、特定の水準が支配的な状況でない限りは、円グラフの採用には慎重になりましょう。

また、各円グラフはあくまでも各年代の 内訳 を表現しているに過ぎず、その 絶対数 を表現しているわけではありません。 例えば、週刊少年サンデーの作者数を見てみると、

Hide code cell content
# 週刊少年サンデーのデータを表示
df_cm2[df_cm2["マンガ雑誌名"] == "週刊少年サンデー"]
マンガ雑誌名 年代 作者数
0 週刊少年サンデー 1970 95
1 週刊少年サンデー 1980 157
2 週刊少年サンデー 1990 147
3 週刊少年サンデー 2000 162
4 週刊少年サンデー 2010 179

となっており、2000年から2010年にかけて17人 増加 していますが、円グラフ上の割合は20.6%から20.3%減少 しています。 これは、2010年の全マンガ作者数(分母)が大きかったことが原因です。 絶対数と内訳の逆転現象はよく起こるので、分母は常に意識しましょう。

最後にデータに関する注意です。 今回の集計方法においては、長期間掲載経験のあるマンガ作者は各年代で重複してカウントされています。 言い換えると、上図はある年代・あるマンガ雑誌に 掲載されていた マンガ作者数の内訳であり、 デビューした マンガ作者数の内訳ではありません。 データの前処理と可視化を分けて考えることはできません[貴裕, 2023]。 用途や目的に応じて、適切なデータ、前処理、そして可視化手法を選択しましょう。

アニメデータ#

本データ中の声優の男女比[4]を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ac_act = pd.read_csv(DIR_AN / FN_AC_ACT)

重複してカウントされないよう、本データ中の全ての声優が一つのgenderと紐づいていることを確認します。

Hide code cell content
# 各crtidに対して、関連するgenderのユニークな数を数える
# groupbyでcrtidごとにグループ化し、nuniqueメソッドで各グループのgenderのユニークな数を数える
# all関数を使って、nuniqueの結果が全て1であること(各crtidが1つのgenderにのみ紐づいていること)を確認
assert all(df_ac_act.groupby("actid")["gender"].nunique() == 1)

AssertionErrorが出なかったため、このまま分析を進めます。

Hide code cell content
# 性別ごとの声優数を集計
# 性別を基準にグループ化して、ユニークな声優IDの数(声優数)をカウント
df_an = df_ac_act.groupby("gender")["actid"].nunique().reset_index(name="n_act")

# カラム名をわかりやすく変更("性別"、"声優数")
df_an = df_an.rename(columns={"gender": "性別", "n_act": "声優数"})

# 声優数が多い順に配置されるように修正
orders_an = {"性別": df_an.sort_values("声優数", ascending=False)["性別"].tolist()}
Hide code cell content
# 可視化対象のDataFrameを確認
df_an.head()
性別 声優数
0 female 1664
1 male 1334
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an, DIR_OUT_AN, "an")
DataFrame is saved as '../../data/an/output/07/pie/an.csv'.
Hide code cell source
# 性別ごとの声優数を円グラフで可視化

# px.pie関数を使用して、性別ごとの声優数の割合を円グラフで表示
# valuesに声優数、namesに性別を指定してデータをマッピング
# category_ordersで配置順序を指定
# color_discrete_sequenceでOKABE_ITOスケールを指定
fig = px.pie(
    df_an,
    values="声優数",
    names="性別",
    category_orders=orders_an,
    color_discrete_sequence=OKABE_ITO,
)

# 凡例名を追加
fig.update_layout(legend_title_text="声優の性別")

# 作成した円グラフを表示
show_fig(fig)

上図は、本データ中の声優の男女比を示した円グラフです。男性(male)より女性(female)の声優の方が10ポイントほど多いことがわかります。

マンガデータの円グラフと異なり、質的変数間の内訳の大小関係の把握が圧倒的に容易です。 質的変数の水準が二つの場合は、扇形のどちらかの端点が必ず固定されているためです。 水準が三つ以上になると、中途半端な角度にずれた扇形が出てくるため、ひと目で比較することが難しくなります。

では、年代ごとにどの様に割合が変化するか見てみましょう。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ae = pd.read_csv(DIR_AN / FN_AE)
Hide code cell content
# df_aeに年代情報を追加するための関数add_years_to_dfを適用
# unit_years=5を指定することで、5年単位の年代情報を追加する
df_ae = add_years_to_df(df_ae, unit_years=5)

# acidとyears(5年単位の年代)のユニークな組み合わせを持つDataFrameを作成
# 重複するデータを削除し、必要なカラムのみを選択
# ignore_index=Trueで新しいインデックスを割り当てる
df_ac_years = df_ae.drop_duplicates(subset=["acid", "years"], ignore_index=True)[
    ["acid", "years"]
]
Hide code cell content
# df_ac_actとdf_ac_yearsをacidを基準にして左結合(left join)し、df_mergeを作成
# これにより、df_ac_actのデータにdf_ac_yearsの年代情報が組み合わされる
df_merge = pd.merge(df_ac_act, df_ac_years, on="acid", how="left")

# df_mergeを性別と年代ごとにグループ化し、声優のユニーク数を集計
# nunique()を使用して各グループ内のユニークな声優数をカウント
df_an2 = (
    df_merge.groupby(["gender", "years"])["acid"].nunique().reset_index(name="n_act")
)

# 列名をわかりやすい名前に変更
df_an2 = df_an2.rename(columns={"gender": "性別", "years": "年代", "n_act": "声優数"})

マンガデータの例と同様、内訳の分母として年代ごとの合計声優数を用います。 分母が小さいと内訳が極端な値を取りやすくなるため、可視化には向きません。

事前に年代ごとの声優数を集計しておきましょう。

Hide code cell content
# 年代別の声優数を集計
df_an2.groupby("年代")["声優数"].sum().reset_index()
年代 声優数
0 1960 4
1 1965 4
2 1970 6
3 1975 4
4 1980 2
5 1990 8
6 1995 137
7 2000 1113
8 2005 1695
9 2010 1654
10 2015 1149

1995年代まで、分母に相当する声優数が極端に小さいことがわかりました。 可視化結果を安定させるため、2000年以降を可視化対象としましょう。

Hide code cell content
# 2000年代以降を可視化対象とする
df_an2 = df_an2[df_an2["年代"].astype(int) >= 2000].reset_index(drop=True)
Hide code cell content
# 可視化対象のDataFrameを確認
df_an2.head()
性別 年代 声優数
0 female 2000 561
1 female 2005 844
2 female 2010 856
3 female 2015 597
4 male 2000 552
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an2, DIR_OUT_AN, "an2")
DataFrame is saved as '../../data/an/output/07/pie/an2.csv'.
Hide code cell source
# 性別ごとの声優数を円グラフで可視化

# px.pie関数を使用して、性別ごとの声優数の割合を円グラフで表示
# valuesに声優数、namesに性別を指定してデータをマッピング
# facet_colとして年代を指定することで、年代別の円グラフを表示
# category_ordersで配置順序を指定
# color_discrete_sequenceでOKABE_ITOスケールを指定
fig = px.pie(
    df_an2,
    values="声優数",
    names="性別",
    facet_col="年代",
    category_orders=orders_an,
    color_discrete_sequence=OKABE_ITO,
)

# 凡例名を追加
fig.update_layout(legend_title_text="声優の性別")

# ファセット(年代ごとの円グラフ)のタイトルを簡潔にする処理
# デフォルトではタイトルは「年代=xxx」という形式になっている
# この処理は「=」で文字列を分割して「xxx」の部分だけを取り出す
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成した円グラフを表示
show_fig(fig)

上図は、年代別の声優の性別の内訳を表現した円グラフです。 2005年までほぼ同率ですが、以降徐々に女性声優の割合が増えていることがわかります。

ところで、category_ordersオプションを指定しないとどのような可視化になるでしょうか?

Hide code cell source
# 性別ごとの声優数を円グラフで可視化

# px.pie関数を使用して、性別ごとの声優数の割合を円グラフで表示
# valuesに声優数、namesに性別を指定してデータをマッピング
# category_ordersで配置順序を指定
# color_discrete_sequenceでOKABE_ITOスケールを指定
fig = px.pie(
    df_an2,
    values="声優数",
    names="性別",
    facet_col="年代",
    # category_orders=orders_an,
    color_discrete_sequence=OKABE_ITO,
)

# 凡例名を追加
fig.update_layout(legend_title_text="声優の性別")

# ファセット(年代ごとの円グラフ)のタイトルを簡潔にする処理
# デフォルトではタイトルは「年代=xxx」という形式になっている
# この処理は「=」で文字列を分割して「xxx」の部分だけを取り出す
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成した円グラフを表示
show_fig(fig)

円グラフごとに性別の順序が変わってしまい、非常に見づらくなってしまいました。 facetオプションを使って複数の円グラフを同時に作成する際は、category_ordersで順序を固定することをおすすめします。

最後に、データに関する注意です。 今回の集計方法では、長期間活動した声優は各年代で重複してカウントされています。 言い換えると、上図はある年代・ある性別の 活動実績のある 声優の内訳であり、 デビューした 声優の内訳ではありません。

ゲームデータ#

発売曜日ごとのゲームパッケージ数を例に、可視化手法を解説します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_pkg_pf = pd.read_csv(DIR_GM / FN_PKG_PF)
Hide code cell content
# date列をdatetimeオブジェクトに変換して、曜日情報を新たな列としてdf_pkg_pfに追加
df_pkg_pf["weekday"] = pd.to_datetime(df_pkg_pf["date"]).dt.weekday

重複カウントを防ぐため、一つのゲームパッケージはかならず一つの発売曜日と紐づいていることを確認します。

Hide code cell content
# 各pkgidに対して、関連するweekdayのユニークな数を数える
# groupbyでpkgidごとにグループ化し、nuniqueメソッドで各グループのweekdayのユニークな数を数える
# all関数を使って、nuniqueの結果が全て1であること(各pkgidが1つのweekdayにのみ紐づいていること)を確認
assert all(df_pkg_pf.groupby("pkgid")["weekday"].nunique() == 1)

AssertionErrorが出なかったため、このまま分析を進めます。

Hide code cell content
# 曜日ごとのパッケージ数を集計するためのデータ前処理

# 曜日ごとにユニークなパッケージIDの数を集計
df_gm = df_pkg_pf.groupby("weekday")["pkgid"].nunique().reset_index(name="n_pkg")

# 数値で表されている曜日を文字列にマッピング
df_gm["yobi"] = df_gm["weekday"].apply(lambda x: WEEKDAY2YOBI.get(x, None))

# 列名をリネーム
df_gm = df_gm.rename(columns={"n_pkg": "パッケージ数", "yobi": "発売曜日"})

# パッケージ数順に表示されるように指定する場合
orders_gm_pkg = {
    "発売曜日": df_gm.sort_values("パッケージ数", ascending=False)["発売曜日"].tolist()
}
Hide code cell content
# 可視化対象のDataFrameを確認
df_gm.head()
weekday パッケージ数 発売曜日
0 0 281
1 1 1692
2 2 4515
3 3 22844
4 4 5331
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm, DIR_OUT_GM, "gm")
DataFrame is saved as '../../data/gm/output/07/pie/gm.csv'.
Hide code cell source
# 曜日ごとのパッケージ数を円グラフで可視化

# px.pieを使用して、曜日ごとのパッケージ数の円グラフを作成
# df_gmデータフレームを使用し、'発売曜日'列を名前、'パッケージ数'列を値としてプロット
# 表示順序が割合の降順になるよう、category_ordersで指定
# OKABE_ITOカラースキームを使用
fig = px.pie(
    df_gm,
    values="パッケージ数",
    names="発売曜日",
    category_orders=orders_gm_pkg,
    color_discrete_sequence=OKABE_ITO,
)

# 凡例名を追加
fig.update_layout(legend_title_text="発売曜日")

# 作成した円グラフを表示
show_fig(fig)

上図は、曜日別のゲームパッケージ発売数の内訳を示した円グラフです。 60%以上のゲームパッケージが曜日に発売されていました。 次いで、曜あるいは曜に約30%のゲームパッケージが発売されていました。 その他の曜日での発売は例外的です。

内訳の差が大きい場合は、三つ以上の水準をもつ質的変数を可視化しても見づらくありません。 上図の場合、可視化結果として注目されるのはせいぜい上位三つの水準までであり、それ以外は注意の対象となりづらい[5]ことが原因の一つと考えられます。

一方でマンガデータの円グラフは、水準は上図より少ないですが、それぞれの内訳に大きな差がありません。 注目するべき水準を絞れないため、見づらく感じてしまうのでしょう。

上図では内訳が大きい順に表示しましたが、曜始まりの曜日順で表示することもできます。

Hide code cell source
# 曜日順に表示されるように指定する場合
orders_gm_weekday = {"発売曜日": df_gm.sort_values("weekday")["発売曜日"].tolist()}

# px.pieを使用して、曜日ごとのパッケージ数の円グラフを作成
# df_gmデータフレームを使用し、'発売曜日'列を名前、'パッケージ数'列を値としてプロット
# 表示順序が曜日順になるよう、category_ordersで指定
# OKABE_ITOカラースキームを使用
fig = px.pie(
    df_gm,
    values="パッケージ数",
    names="発売曜日",
    category_orders=orders_gm_weekday,
    color_discrete_sequence=OKABE_ITO,
)

# 凡例名を追加
fig.update_layout(legend_title_text="発売曜日")

# 作成した円グラフを表示
show_fig(fig)

曜日順にしたことで、右側の凡例は理解しやすくなりましたが、肝心の円グラフから情報を読み取りづらくなりました。 例えば、 一目で どの曜日が最も割合が大きいかわかるでしょうか?

次は、年代ごとの変遷を可視化してみましょう。

Hide code cell content
# df_pkg_pfに年代(5年刻み)を追加
df_pkg_pf = add_years_to_df(df_pkg_pf, unit_years=5)
Hide code cell content
# years、weekdayごとにpkgidのユニーク数を集計し、n_pkg列として追加
df_gm2 = (
    df_pkg_pf.groupby(["years", "weekday"])["pkgid"].nunique().reset_index(name="n_pkg")
)

# 数値で表されている曜日を文字列にマッピング
df_gm2["yobi"] = df_gm2["weekday"].apply(lambda x: WEEKDAY2YOBI.get(x, None))

# 可視化用に列名をリネーム
df_gm2 = df_gm2.rename(
    columns={"years": "年代", "yobi": "発売曜日", "n_pkg": "パッケージ数"}
)

内訳の分母として、年代ごとのゲームパッケージ数を用います。 分母が小さいと、極端な内訳の値が算出されやすくなるため、可視化には向きません。 そこで、事前に年代ごとの合計パッケージ数を確認しておきましょう。

Hide code cell content
# 年代別の合計ゲームパッケージ数を集計
df_gm2.groupby("年代")["パッケージ数"].sum().reset_index()
年代 パッケージ数
0 1980 53
1 1985 453
2 1990 2074
3 1995 4590
4 2000 5587
5 2005 7637
6 2010 9344
7 2015 5900

1985年代まで比較的分母(パッケージ数)が小さいことがわかりました。 今回の分析では、1990年代以降を可視化対象としましょう。

Hide code cell content
# 1990年代以降を抽出して、可視化対象とする
df_gm2 = df_gm2[df_gm2["年代"].astype(int) >= 1990].reset_index(drop=True)
Hide code cell content
# 可視化対象のDataFrameを確認
df_gm2.head()
年代 weekday パッケージ数 発売曜日
0 1990 0 34
1 1990 1 81
2 1990 2 66
3 1990 3 122
4 1990 4 1559
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm2, DIR_OUT_GM, "gm2")
DataFrame is saved as '../../data/gm/output/07/pie/gm2.csv'.
Hide code cell source
# 曜日ごとのパッケージ数を円グラフで可視化

# px.pieを使用して、曜日ごとのパッケージ数の円グラフを作成
# df_gm2データフレームを使用し、'発売曜日'列を名前、'パッケージ数'列を値としてプロット
# facet_colとして年代を指定することで、年代別の円グラフを表示
# 表示順序が曜日順になるよう、category_ordersで指定
# OKABE_ITOカラースキームを使用
fig = px.pie(
    df_gm2,
    values="パッケージ数",
    names="発売曜日",
    facet_col="年代",
    category_orders=orders_gm_weekday,
    color_discrete_sequence=OKABE_ITO,
)

# 凡例名を追加
fig.update_layout(legend_title_text="発売曜日")

# ファセット(年代ごとの円グラフ)のタイトルを簡潔にする処理
# デフォルトではタイトルは「年代=xxx」という形式になっている
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成した円グラフを表示
show_fig(fig)

上図は、ゲームパッケージの発売曜日の割合を年代別に表現した円グラフです。 1990年代まで曜発売のゲームパッケージが全体の70%以上を占めていましたが、1995年代以降曜日が増えはじめ、2000年代には約80%を占めたことがわかりました。 また、2005年代ごろから曜日発売も増え始めたことがわかります。

ちなみに、ゲームパッケージの発売日として週末直前の平日(曜日)が多い理由の一つとして、買ってすぐ遊んでほしいというメーカー側の思いがあると言われています[友介, 2020]